<?php
/*********************************************
 *  CPG Dragonfly™ CMS
 *********************************************
	Copyright © since 2012 by CPG-Nuke Dev Team
	http://dragonflycms.org

	Dragonfly is released under the terms and conditions
	of the GNU GPL version 2 or any later version
**********************************************/

/*
0:200 = ip:shield
0:203 = ip
0:204 = unknown user-agent
0:800 = ip:bad
0:802 = not compilant user-agent
0:803 = ip:flood
1:200 = bot:verified
1:203 = bot:
1:800 = bot:bad
1:802 = bot:empty
1:803 = bot:flood
3:801 = referer:bad
4:801 = email, referer:bad (deprecated)
6:200 = mac:verified-
6:800 = mac:bad-
9:200 = hostname:verified
9:800 = hostname:bad
10:800 = in_dns_bl
*/

class Security
{

	public static function init()
	{
		# Show error page if the http server sends an error
		if (isset($_SERVER['REDIRECT_STATUS']) && between($_SERVER['REDIRECT_STATUS'], 400, 503)) {
			cpg_error('', $_SERVER['REDIRECT_STATUS']);
		}
		if (isset($_SESSION['SECURITY']['status']) && 300 < $_SESSION['SECURITY']['status']) { cpg_error('', $_SESSION['SECURITY']['status']); }

		global $db, $prefix, $MAIN_CFG, $SESS;
		if ($SESS->new) {
			$time = time();
			$expire = $time + $MAIN_CFG->_security->cachettl;
			$ipn = Dragonfly_Net::ipn();
			$data = array(
				'ipn' => $ipn,
				'status' => 203,
				'type' => 0,
				'ttl' => $expire,
				'log' => empty($_SERVER['HTTP_USER_AGENT']) ? '' : $_SERVER['HTTP_USER_AGENT']
			);

			$result = $db->query('SELECT status, name, hostname, type, ttl FROM '.$prefix.'_security_cache WHERE ipn='.$db->escapeBinary($ipn).' LIMIT 1');
			if ($cached = $db->affected_rows()) {
				$tmp = $result->fetch_assoc();
				if (intval($tmp['ttl']) < $time) {
					$result->free();
					$tmp = null;
					$cached = 0;
					$db->query('DELETE QUICK FROM '.$prefix.'_security_cache WHERE ttl < '.$time);
					$db->optimize($db->TBL->security_cache);
					$data = array_merge($data, Security::exec());
				} else {
					$result->free();
					$data = array_merge($data, $tmp);
					$tmp = null;
					if (200 == $data['status']) {
						$db->query("UPDATE {$prefix}_security_cache SET ttl=$expire WHERE ipn=".$db->escapeBinary($ipn));
					}
				}
			} else {
				$data = array_merge($data, Security::exec());
			}
			Client::$name = empty($data['name']) ? Client::$name : $data['name'];
			if (!empty($data['hostname'])) { Client::$hostname = $data['hostname']; }
			if (1 == $data['type'])        { Client::$engine   = 'bot'; }
			if (200 == $data['status'])    { Client::$verified = true; }

			if (!$cached) {
				if (isset($data['name']))     { $data['name'] = $db->quote($data['name']); }
				if (isset($data['hostname'])) { $data['hostname'] = $db->quote($data['hostname']); }
				$data['ipn'] = $db->escapeBinary($data['ipn']);
				$data['log'] = $db->quote($data['log']);
				$db->query('INSERT INTO '.$prefix.'_security_cache ('.implode(', ', array_keys($data)).') VALUES ('.implode(', ', $data).')');
			}
			$_SESSION['SECURITY']['status'] = $data['status'];
		}
		define('SEARCHBOT', ('bot' == Client::$engine) ? Client::$name : false);

		$message = isset($data) && 10 == $data['type'] ? '<br />From: '.htmlprepare($data['log']) : '';
		if (300 < $_SESSION['SECURITY']['status']) cpg_error('', $_SESSION['SECURITY']['status']);

	}

	private static function exec()
	{
		global $db, $prefix, $MAIN_CFG;
		$ip = Dragonfly_Net::ip();
		$ipn = $db->escapeBinary(inet_pton($ip));

		if ($MAIN_CFG->_security->ips) {
			if ($row = $db->uFetchAssoc("SELECT type, details FROM {$prefix}_security_ips WHERE ipn_s=$ipn OR ipn_s < $ipn AND ipn_e >= $ipn")) {
				if (0 == $row['type']) return array('status' => 800, 'log' => $row['details']);
				if (8 == $row['type']) return array('status' => 200, 'log' => $row['details']);
			}
		}

		$data = array();
		$bot = $reverse = false;
		if ($MAIN_CFG->_security->hostnames || $MAIN_CFG->_security->bots) {
			$reverse = Dragonfly_Net_Dns::reverse($ip);
			if (is_array($reverse) && !empty($reverse['hostname'])) {
				$data['hostname'] = $reverse['hostname'];
			}
		}
		if (!empty($data['hostname']) && $MAIN_CFG->_security->hostnames && $domain = Security::detectDomain(substr($data['hostname'], 0, -1))) {
			$data['type'] = 9;
			if (!empty($domain['ban_string'])) $data['log'] = $domain['ban_string'];
			if (9 == $domain['ban_type']) {
				$data['status'] = 800;
				return $data;
			} else if (8 == $domain['ban_type'] && $reverse['verified']) {
				$data['status'] = 200;
				return $data;
			}
		}
		if ($bot = Security::detectBot()) {
			$data['type'] = 1;
			$data['name'] = $bot['agent_name'];
			if ($MAIN_CFG->_security->bots) {
				if (-1 == $bot['agent_ban']) {
					$data['status'] = 802;
					return $data;
				} else if (!empty($data['hostname']) && !empty($bot['agent_hostname']) && $reverse['verified']) {
					if (false === strpos($bot['agent_hostname'], '\\')) $bot['agent_hostname'] = preg_quote($bot['agent_hostname']);
					if (preg_match('#'.$bot['agent_hostname'].'\.$#i', $data['hostname'])) {
						$data['status'] = 200;
						return $data;
					}
				}
			}
		} else if ('SERVFAIL' === $reverse) {
			$data['ttl'] = time() + ($MAIN_CFG->_security->cachettl - 60);
		} else if ('NXDOMAIN' === $reverse /* && $MAIN_CFG->_security->emptyhost*/) {
			$data['log'] = 'Empty domain';
			//$data['status'] = 802;
		}

		if (!$bot && empty(Client::$name)) {
			$data['status'] = 204;
			if ($MAIN_CFG->_security->uas && (empty($_SERVER['HTTP_USER_AGENT']) || !preg_match('#^[a-zA-Z]#', $_SERVER['HTTP_USER_AGENT']))) {
				$data['status'] = 802;
				$data['ttl'] = time() + $MAIN_CFG->_security->bantime;
				return $data;
			}
		}

		# Check for dns blacklisted IPs
		if ($MAIN_CFG->_security->dns_bl_active && $bl = Security::dns_blocklist($ip)) {
			$data['type'] = 10;
			$data['status'] = 800;
			$data['ttl'] = time() + $MAIN_CFG->_security->bantime;
			$data['log'] = $bl;
			return $data;
		}

		# Referer spam?
		if ($MAIN_CFG->_security->referers
		    && !empty($_SERVER['HTTP_REFERER'])
		    && false === strpos($_SERVER['HTTP_REFERER'], $MAIN_CFG->server->domain)
		    && !Security::check_domain($_SERVER['HTTP_REFERER']))
		{
			$data['status'] = 801;
			$data['type'] = 3;
			$data['ttl'] = time() + $MAIN_CFG->_security->bantime;
		}
		return $data;
	}

	public static function check()
	{
		global $MAIN_CFG;
		# anti-flood protection
		if ($MAIN_CFG->_security->flooding && 200 != $_SESSION['SECURITY']['status'])
			Security::flood();
	}

	public static function check_post()
	{
		if ($_SERVER['REQUEST_METHOD'] != 'POST') { return false; }
		global $module_name;
		if (empty($_SESSION['SECURITY']['page']) || $module_name != $_SESSION['SECURITY']['page']) { cpg_error(_SEC_ERROR, _ERROR_BAD_LINK, URL::admin()); }
		return true;
	}

	public static function check_domain($domain)
	{
		if (!preg_match('#[^\./]+\.[\w]+($|/)#', $domain)) { return false; }
		$domains = '';
		global $db, $prefix;
		if ($result = $db->query('SELECT ban_string FROM '.$prefix."_security_domains WHERE ban_type IN (3,4)", TRUE, TRUE)) {
			while ($e = $db->fetch_array($result, SQL_NUM)) { $domains .= "|$e[0]"; }
		}
		if (empty($domains)) { return true; }
		return (preg_match('#('.str_replace('.', '\.', substr($domains,1).')#i'), $domain) < 1);
	}

	public static function get_ip()
	{
		trigger_deprecated("Use Dragonfly_Net::ipn() instead.");
		return Dragonfly_Net::ipn();
	}

	public static function dns_blocklist($ip)
	{
		global $MAIN_CFG;
		if (!$MAIN_CFG->_security->dns_bl_active || !$ip = Dragonfly_Net::filterIP($ip, false)) return false;
		if ($ip['v4']) {
			$ip = implode('.', array_reverse(explode('.', $ip['ip']))).'.';
		} else {
			$ip = unpack('H*', $ip['ipn']);
			$ip = implode('.', array_reverse(str_split($ip[1]))).'.';
		}
		for ($i=1; $i<=3; ++$i) {
			$whitelist = array();
			if (!$bl = $MAIN_CFG->_security->{"dns_bl_srv_$i"}) continue;
			$response = Dragonfly_Net_Dns::resolve($ip.$bl);
			if (is_array($response) && isset($response['A'][$ip.$bl.'.'][0])) {
				$response = $response['A'][$ip.$bl.'.'][0];
			} else {
				continue;
			}
			if (!$exclude = $MAIN_CFG->_security->{"dns_bl_exc_$i"}) return $bl ." ($response)";
			if (0 === strpos($exclude, 'b:')) {
				$exclude = intval(substr($exclude, 2));
				$ret = explode('.', $response);
				if ((255 - $exclude) & intval($ret[3])) return $bl ." ($response)";
			} else {
				$ret = array_map('trim', explode(',', $exclude));
				if ($ret && !in_array($response, $ret)) return $bl ." ($response)";
			}
		}
		return false;
	}

	private static function flood()
	{
		global $db, $prefix, $MAIN_CFG;
		$ip = Dragonfly_Net::ipn();
		$ipn = $db->binary_safe($ip);
		$delay = $MAIN_CFG->_security->delay;
		$flood_time = $flood_count = 0;
		$log = array();
		$time = time();
		if (!isset($_SESSION['SECURITY']['flood_start'])) {
			$db->query('DELETE FROM '.$prefix.'_security_flood WHERE flood_time <= '.$time);
		} else {
			$_SESSION['SECURITY']['flood_start'] = false;
		}

		if ($MAIN_CFG->_security->debug || empty($_SESSION['SECURITY']['flood_time'])) {
			# try to load time from log
			if ($row = $db->sql_ufetchrow('SELECT * FROM '.$prefix.'_security_flood WHERE flood_ip ='.$ipn, SQL_ASSOC)) {
				if (!empty($row)) {
					$flood_time = $row['flood_time'];
					$flood_count = $row['flood_count'];
					if (!empty($row['log']) && $MAIN_CFG->_security->debug) {
						$log = unserialize($row['log']);
					}
				}
			}
		} else {
			$flood_time = $_SESSION['SECURITY']['flood_time'];
			$flood_count = $_SESSION['SECURITY']['flood_count'];
		}
		if ($flood_time >= $time) {
			# die with message and report
			++$flood_count;
			if ($flood_count <= 5) {
				if ($flood_count > 2 && $flood_count <= 5) {
					Security::flood_log($ipn, !empty($row), $delay, $time, $log, $flood_count);
					global $LNG;
					Dragonfly::getKernel()->L10N->load('errors');
					$flood_time = (($flood_count+1)*2)/$delay;
					header($_SERVER['SERVER_PROTOCOL'].' 503 Service Unavailable');
					header('Retry-After: '.$flood_time);
					$msg = sprintf($LNG['_SECURITY_MSG']['_FLOOD'], $flood_time);
					if ($flood_count == 5) { $msg .= $LNG['_SECURITY_MSG']['Last_warning']; }
					$msg = sprintf(_JS_ALERT, $msg);
					cpg_error($msg, 'Flood Protection');
				}
			} else {
				if ($MAIN_CFG->_security->debug) {
					if (!empty($log)) { $log = Security::log_serializer($log); }
					else if (!empty($_SESSION['FLOODING'])) { $log = Security::log_serializer($_SESSION['FLOODING']); }
					$log = "'$log'";
				} else {
					$log = 'DEFAULT';
				}
				$ttl = $time + $MAIN_CFG->_security->bantime;
				$db->query('UPDATE '.$prefix."_security_cache SET status=803, ttl=$ttl, log=$log WHERE ipn=".$ipn);
				global $SESS;
				if (is_object($SESS)) $SESS->destroy();
				cpg_error('', 803);
			}
		} else {
			$log = null;
			$flood_count = 0;
			$_SESSION['FLOODING'] = array();
		}
		Security::flood_log($ipn, !empty($row), $delay, $time, $log, $flood_count);
	}

	private static function detectBot()
	{
		if (empty($_SERVER['HTTP_USER_AGENT'])) return;
		global $db, $prefix;
		$bot = false;
		# Identify bot by UA
		$result = $db->query('SELECT agent_name, agent_fullname, agent_ban, agent_hostname FROM '.$prefix.'_security_agents');
		while ($row = $result->fetch_assoc()) {
			if (empty($row['agent_fullname'])) { continue; }
			if (preg_match('#'.preg_quote($row['agent_fullname']).'#i', $_SERVER['HTTP_USER_AGENT'])) {
				$bot = $row;
				break;
			}
		}
		$result->free();
		if (!$bot && false !== stripos($_SERVER['HTTP_USER_AGENT'], 'bot')) {
			$bot['agent_name'] = 'Crawler';
			$bot['agent_ban'] = 0;
		}
		return $bot;
	}

	public static function detectDomain($hostname)
	{
		if (!Dragonfly_Net::validHostname($hostname)) return false;
		global $db, $prefix;
		$domain = false;
		$result = $db->query('SELECT ban_string, ban_type FROM '.$prefix.'_security_domains WHERE ban_type IN (8,9)');
		while ($row = $result->fetch_assoc()) {
			if (empty($row['ban_string'])) continue;
			if (false === strpos($row['ban_string'], '\\')) $row['ban_string'] = preg_quote($row['ban_string']);
			if (preg_match('#'.$row['ban_string'].'$#i', $hostname)) {
				$row['ban_string'] = str_replace('\\', '', $row['ban_string']);
				$domain = $row;
				break;
			}
		}
		$result->free();
		return $domain;
	}

	private static function flood_log($ip, $update=false, $delay, $time, $log, $times)
	{
		global $MAIN_CFG;
		$timeout = ((($times+1)*2)/$delay)+time();
		# maybe the UA doesn't accept cookies so we use another session log as well
		if (empty($_SESSION['SECURITY']['flood_time']) || $MAIN_CFG->_security->debug) {
			global $db, $prefix;
			if ($MAIN_CFG->_security->debug) { $log = "'".Security::log_serializer(Security::data_log($times, $log))."'"; }
			else { $log = 'DEFAULT'; }
			if ($update) {
				$db->query('UPDATE '.$prefix."_security_flood SET flood_time=$timeout, flood_count=$times, log=$log WHERE flood_ip=$ip");
			} else {
				$db->query('INSERT INTO '.$prefix."_security_flood (flood_ip,flood_time,flood_count,log) VALUES ($ip, '$timeout', '$times', $log)", true);
			}
			$_SESSION['SECURITY']['flood_start'] = true;
		}
		$_SESSION['SECURITY']['flood_time'] = $timeout;
		$_SESSION['SECURITY']['flood_count'] = $times;
	}

	private static function detectProxy()
	{
		if (SEARCHBOT) { return Client::$name; }
		if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) return $_SERVER['HTTP_X_FORWARDED_FOR'];
		if (!empty($_SERVER['VIA'])) return $_SERVER['VIA'];
		$rating = 0;
		if ($_SERVER['SERVER_PROTOCOL'] == 'HTTP/1.0') $rating += 1;
		if (!isset($_SERVER['HTTP_ACCEPT']) || $_SERVER['HTTP_ACCEPT'] == '*/*') $rating += 3;
		if (intval($_SERVER['REMOTE_PORT']) > 5000) $rating += 5;
		if (!$rating || $rating == 1) return 'None';
		if ($rating <= 4) $rating = 'Probably anonymous';
		else $rating = 'Yes, anonymous';
		return $rating;
	}

	private static function data_log($c, $l)
	{
		$l[$c]['S_TIME'] = time();
		$l[$c]['S_USER'] = empty($_SESSION['DF_VISITOR']) ? '' : $_SESSION['DF_VISITOR']->identity->nickname;
		$l[$c]['S_UA'] = $_SERVER['HTTP_USER_AGENT'];
		$l[$c]['S_ADDRESS'] = $_SERVER['REMOTE_ADDR'];
		$l[$c]['S_METHOD'] = $_SERVER['REQUEST_METHOD'];
		$l[$c]['S_URI'] = $_SERVER['REQUEST_URI'];
		$l[$c]['S_REFERER'] = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : '';
		# if proxy is behind a firewall then bypass-client will contain the firewall ip
		$l[$c]['S_PROXY'] = self::detectProxy();
		$_SESSION['FLOODING'][$c] = $l[$c];
		return $l;
	}

	private static function log_serializer($log)
	{
		return Fix_Quotes(serialize($log));
	}
}
